package Zim::GUI::PageView;

use strict;
use vars qw/$CODESET/;
use POSIX qw(strftime);
use Encode;
use Gtk2;
use Gtk2::Pango;               # pango constants
use Gtk2::Ex::HyperTextView;   # custom widget
use Gtk2::Ex::HyperTextBuffer; #    "     "
use Gtk2::Gdk::Keysyms;	       # key identifiers
use File::Spec;
use File::MimeInfo::Magic;
use File::BaseDir qw/xdg_data_files xdg_data_home/;
use Zim::GUI::Component;

eval "use Gtk2::Ex::DesktopEntryMenu";
my $has_mimeapplications = $@ ? 0 : 1;

our $VERSION = '0.20';

our @ISA = qw/Zim::GUI::Component/;

*CODESET = \$Zim::CODESET;
$CODESET ||= 'utf8';

# TODO move more logic into Gtk2::Ex::HyperTextBuffer !

=head1 NAME

Zim::GUI::PageView - Page TextView widgets

=head1 DESCRIPTION

This module contains the widgets to display an editable
text buffer containing the current page. It includes a search entry
at the bottom of the TextView, and autoformatting logic.

=head1 METHODS

Undefined methods are AUTOLOADED to the Gtk2::Ex::HyperTextView object.

=over 4

=cut

my $ui_format_actions =
# name,		stock id,	label,		accelerator,	tooltip
qq{
Head1		.		Head _1		<ctrl>1		Heading 1
Head2		.		Head _2		<ctrl>2		Heading 2
Head3		.		Head _3		<ctrl>3		Heading 3
Head4		.		Head _4		<ctrl>4		Heading 4
Head5		.		Head _5		<ctrl>5		Heading 5
Normal		.		_Normal		<ctrl>6		Normal
Bold		gtk-bold	_Bold		<ctrl>B		Bold
Italic		gtk-italic	_Italic		<ctrl>I		Italic
Underline	gtk-underline	_Underline	<ctrl>U		Underline
Strike		gtk-strikethrough	Stri_ke	<ctrl>K		Strike
Verbatim	.		_Verbatim	<ctrl>T		Verbatim
};

my $ui_format_toggle_actions =
qq{
TLink		gtk-connect	_Link		.		Link
TBold		gtk-bold	_Bold		.		Bold
TItalic		gtk-italic	_Italic		.		Italic
TUnderline	gtk-underline	_Underline	.		Underline
TStrike		gtk-strikethrough	Stri_ke	.		Strike
};

my $ui_actions =
qq{
Undo		gtk-undo	_Undo		<ctrl>Z		Undo
Redo		gtk-redo	_Redo		<ctrl><shift>Z	Redo
Delete		gtk-delete	_Delete		.		Delete
EditLink	gtk-properties	_Edit Link...	<ctrl>E		Edit link
InsertDate	.		Insert _Date and Time...	<ctrl>D	Insert date
InsertLink	gtk-connect	Insert _Link...		.	Insert link
InsertImage	.		Insert _Image...	.	Insert image
FindReplace	gtk-find-and-replace	_Replace...	<ctrl>H	Find and Replace
};

my $ui_copy_paste_actions =
q{
Cut		gtk-cut		Cu_t		<ctrl>X		Cut
Copy		gtk-copy	_Copy		<ctrl>C		Copy
Paste		gtk-paste	_Paste		<ctrl>V		Paste
};

my $ui_actions_ro =
qq{
Link		gtk-connect	_Link		<ctrl>L		Link
Find		gtk-find	_Find...	<ctrl>F		Find
FindNext	.		Find Ne_xt	<ctrl>G		Find next
FindPrev	.		Find Pre_vious	<ctrl><shift>G	Find previous
WordCount	.		_Word Count	.		Word count
};

my $ui_layout_ro = q{<ui>
	<menubar name='MenuBar'>
		<menu action='SearchMenu'>
			<placeholder name='FindItems'>
				<menuitem action='Find'/>
				<menuitem action='FindNext'/>
				<menuitem action='FindPrev'/>
			</placeholder>
		</menu>
		<menu action='ToolsMenu'>
			<placeholder name='PageTools'>
				<menuitem action='WordCount'/>
			</placeholder>
		</menu>
	</menubar>
	<accelerator action='Link'/>
</ui>};

my $ui_layout = q{<ui>
	<menubar name='MenuBar'>
		<menu action='EditMenu'>
			<placeholder name='EditPage'>
				<menuitem action='Undo'/>
				<menuitem action='Redo'/>
				<separator/>
				<menuitem action='Cut'/>
				<menuitem action='Copy'/>
				<menuitem action='Paste'/>
				<menuitem action='Delete'/>
				<separator/>
				<menuitem action='EditLink'/>
				<separator/>
			</placeholder>
			<placeholder name='InsertItems'>
				<menuitem action='InsertDate'/>
				<menuitem action='InsertImage'/>
				<menuitem action='InsertLink'/>
			</placeholder>
		</menu>
		<menu action='FormatMenu'>
			<menuitem action='Head1'/>
			<menuitem action='Head2'/>
			<menuitem action='Head3'/>
			<menuitem action='Head4'/>
			<menuitem action='Head5'/>
			<menuitem action='Normal'/>
			<separator/>
			<menuitem action='Bold'/>
			<menuitem action='Italic'/>
			<menuitem action='Underline'/>
			<menuitem action='Strike'/>
			<menuitem action='Verbatim'/>
			<separator/>
			<menuitem action='Link'/>
		</menu>
		<menu action='SearchMenu'>
			<placeholder name='FindItems'>
				<menuitem action='FindReplace'/>
			</placeholder>
		</menu>
	</menubar>
	<toolbar name='ToolBar'>
		<placeholder name='Format'>
			<toolitem action='TLink'/>
			<toolitem action='TBold'/>
			<toolitem action='TItalic'/>
			<toolitem action='TUnderline'/>
			<toolitem action='TStrike'/>
		</placeholder>
		<separator/>
	</toolbar>
</ui>};

sub _user_action_ (&$) {
	# This is a macro needed to make actions undo-able
	# wrap all interactive operations with this method.
	$_[1]->signal_emit('begin_user_action');
	$_[0]->();
	$_[1]->signal_emit('end_user_action');
}

=item C<new(app => PARENT)>

Simple constructor.

=cut

my ($k_tab, $k_l_tab, $k_return, $k_kp_enter, $k_backspace, $k_escape, $k_multiply, $k_home, $k_F3) =
	@Gtk2::Gdk::Keysyms{qw/Tab ISO_Left_Tab Return KP_Enter BackSpace Escape KP_Multiply Home F3/};
#my @k_parse_word = ($k_tab, map ord($_), ' ', qw/. ; , ' "/);

sub init { # called by new()
	my $self = shift;

	# load style data
	my $file = Zim::GUI::_xdg_config_data_files('zim', 'style.conf');
	my $tags = Zim::File->new($file)->_read_config_alt;
	if (%$tags) {
		no strict 'refs';
		for my $k (keys %$tags) {
			my $list = [
				map { # map constants
					/^PANGO_/ ? &{"Gtk2::Pango::$_"} : $_
				}
				%{$$tags{$k}}
			];
			$Gtk2::Ex::HyperTextBuffer::TAGS{$k} = $list;
		}
	}
	else {
		@Gtk2::Ex::HyperTextBuffer::TAGS{'link', 'underline'}
			= ([foreground => 'blue'], [background => 'yellow']) ;
	}


	my $vbox = Gtk2::VBox->new(0, 0);
	$self->{vbox} = $vbox;
	$self->{top_widget} = $vbox;
	
	my $scrolled_window = Gtk2::ScrolledWindow->new();
	$scrolled_window->set_policy('automatic', 'automatic');
	$scrolled_window->set_shadow_type('in');
	$vbox->add($scrolled_window);
	$self->{scrolled_window} = $scrolled_window;

	# init TextView
	my $htext = Gtk2::Ex::HyperTextView->new();
	$htext->set_left_margin(10);
	$htext->set_right_margin(5);
	$htext->set_editable(0) if $self->{app}{settings}{read_only};
	$htext->set_tabs( Gtk2::Pango::TabArray->new_with_positions(
		# initial_size, pos_in_pixels, ... allign => position
		1, 0, 'left' => 40 * PANGO_SCALE ) );
	$htext->{follow_on_enter} = $self->{app}{settings}{follow_on_enter};
	$htext->signal_connect(link_clicked =>
		sub { $self->{app}->link_clicked($_[1]) }  );
	$htext->signal_connect(link_enter =>
		sub {
			my $p = $self->{app}{page};
			my ($t, $l) = $p->parse_link($_[1]);
			#warn "parsed: $_[1] => $l ($t)\n";
			$l = $p->resolve_name($l) if $t eq 'page';
			$self->{app}->push_status("Go to \"$l\"", 'link');
		}  );
	$htext->signal_connect(link_leave =>
		sub { $self->{app}->pop_status('link') }  );
	$htext->signal_connect_after(toggle_overwrite => \&on_toggle_overwrite, $self);
	$htext->signal_connect(populate_popup => \&on_populate_popup, $self);
	$htext->signal_connect(key_press_event => \&on_key_press_event, $self);
	$htext->drag_dest_set(['motion', 'highlight'],
		['link', 'copy', 'move'] ); # We would prefer only 'link' here, but KDE needs 'move' 
	my $tlist = Gtk2::TargetList->new();
	if (Gtk2->CHECK_VERSION(2, 6, 0)) {
		$tlist->add_uri_targets(0);
		$tlist->add_text_targets(1);
	}
	else { # 2.4
		$tlist->add_table(
			['text/uri-list', [], 0],
			$self->list_text_targets(1) );
	}
	$tlist->add_table(['text/x-zim-page-list', [], 2]);
	$htext->drag_dest_set_target_list($tlist);
	$htext->signal_connect_swapped(
		drag_data_received => \&on_drag_data_received, $self );
	#$htext->signal_connect(drag_motion => \&on_drag_motion); # debug
	$scrolled_window->add($htext);
	$self->{htext} = $htext;
	$self->{widget} = $htext;

	$self->set_font($self->{app}{settings}{textfont})
		if defined $self->{app}{settings}{textfont};

	# init search box
	my $hbox = Gtk2::HBox->new(0, 5);
	$hbox->set_no_show_all(1);
	$hbox->signal_connect(key_press_event => \&on_key_press_event_hbox, $self);
	$vbox->pack_start($hbox, 0, 1, 3);
	$self->{hbox} = $hbox;
	
	my $close_button = Gtk2::Button->new;
	$close_button->set_relief('none');
	$close_button->add(Gtk2::Alignment->new(0.5, 0.5, 0, 0));
	$close_button->child->add(
		Gtk2::Image->new_from_stock('gtk-close', 'small-toolbar') );
	$close_button->signal_connect_swapped(
		clicked => \&CloseFind, $self );
	$hbox->pack_end($close_button, 0,1,0);
	
	$hbox->pack_start( Gtk2::Label->new(' Find: '), 0,1,0);
	
	my $entry = Gtk2::Entry->new();
	$entry->signal_connect(changed => sub { $self->_find($entry->get_text, 0) });
	$entry->signal_connect_swapped(activate => \&on_activate_entry,  $self);
	$hbox->pack_start($entry, 0, 1, 0);
	$self->{entry} = $entry;

	my $prev_button = $self->new_button('gtk-go-back', '_Previous');
	$prev_button->set_sensitive(0);
	$prev_button->signal_connect_swapped(clicked => \&FindPrev, $self);
	$hbox->pack_start($prev_button, 0, 1, 0);
	$self->{find_prev_button} = $prev_button;
	
	my $next_button = $self->new_button('gtk-go-forward', '_Next');
	$next_button->set_sensitive(0);
	$next_button->signal_connect_swapped(clicked => \&FindNext, $self);
	$hbox->pack_start($next_button, 0, 1, 0);
	$self->{find_next_button} = $next_button;

	# add toolbar buttons and key bindings
	$self->add_actions($ui_actions_ro);
	$self->add_ui($ui_layout_ro);
	
	my $accels = $self->{app}{ui}->get_accel_group;
	unless ($self->{app}{settings}{read_only}) {
		$self->add_actions($ui_format_actions, undef, 'ApplyFormat');
		$self->add_actions($ui_format_toggle_actions, 'TOGGLE', 'ApplyFormat');
		$self->add_actions($ui_copy_paste_actions, undef, 'clipboard');
		$self->add_actions($ui_actions);
		$self->add_ui($ui_layout);
		$accels->connect( # ^Y (identical with the shift-^Z defined above)
			ord('Y'), ['control-mask'], ['visible'], sub {$self->Redo} );
	}
	$accels->connect( # alt-/ (identical with the ^F defined above)
		ord('/'), ['mod1-mask'], ['visible'], sub {$self->Find} );
	$accels->connect( $k_F3, [], ['visible'], sub { $self->on_FindNext } );
	$accels->connect( $k_F3, ['shift-mask'], ['visible'], sub { $self->on_FindPrev } );

	# FIXME - prevent our accelerators from triggering while editing another widget
	my $actions = $self->{actions};
	$htext->signal_connect(
		focus_in_event  => sub { $actions->set_sensitive(1) });
	$htext->signal_connect(
		focus_out_event => sub { $actions->set_sensitive(0) });
	
	my $sendto_dir = File::Spec->catdir(xdg_data_home(), 'zim', 'SendTo');
	$self->{sendto_dir} = $sendto_dir if -d $sendto_dir;
	#warn "sendTo dir: $sendto_dir\n";
	
	$self->on_show_cursor;
}

=back

=head2 Actions

=over 4

=item C<ApplyFormat(FORMAT, BOOLEAN)>

Applies the property FORMAT to any selected text
or toggles the edit mode.

BOOLEAN is used to force format (TRUE) or un-format (FALSE). If BOOLEAN is
undefined the function acts as a toggle.

=cut


sub on_ApplyFormat {
	# Catches all for format actions
	#
	# This function passes on boolean values for toggles in order to
	# make sure that the action is consistent with the displayed toggle.
	# Without this it is possible to end up in a infinite loop when the
	# two end up out of sync.
	my ($action, $self) = @_;
	return if $self->{_block_actions};
	my $tag = lc $action->get_name;
	my $bool = undef;
	if ($action->isa('Gtk2::ToggleAction')) {
		$tag =~ s/^t//;
		$bool = $action->get_active;
	}
	warn "## dispatch ACTION " .
		(($tag eq 'link') ? "Link(undef, $bool)" : "ApplyFormat($tag, $bool)") . "\n";
	($tag eq 'link')
		? $self->Link(undef, $bool)
		: $self->ApplyFormat($tag, $bool) ;
}

sub ApplyFormat {
	my ($self, $tag, $bool) = @_;
	die "Can't call ApplyFormat without a tag" unless length $tag;
	my $buffer = $self->{buffer};
	my $has_tag = ($tag eq 'normal') ? 1 : 0;

	# Check selections
	my $_tag = $buffer->get_tag_table->lookup($tag);
	my $_Verbatim;
	my $select;
	my ($start, $end) = $buffer->get_selection_bounds;
	CHECK_TAG:
	if ($end and $start != $end) {
		$select = 'normal';
		$has_tag ||= $start->has_tag($_tag);
	}
	else {
		$has_tag ||= grep {$_ eq $_tag} $buffer->get_edit_mode_tags;
	}
	if (!$has_tag and $tag eq 'verbatim' and ! $_Verbatim) { # ugly exception
		$_Verbatim = $buffer->get_tag_table->lookup('Verbatim');
		$_tag = $_Verbatim;
		goto CHECK_TAG;
	}

	if (!$has_tag and !$select and $self->{app}{settings}{use_autoselect}) {
		($start, $end) = $buffer->auto_selection($tag);
		$select = 'auto' if defined $start;
	}

	return 0 if defined $bool and $bool == $has_tag;
		# do nothing if state is same

	if ($select) { # there is a selection
		if ($tag eq 'verbatim' and !$has_tag) {
			$_Verbatim ||= $buffer->get_tag_table->lookup('Verbatim');
			$tag = 'Verbatim'
				if $start->get_line != $end->get_line
				or $start->starts_line && $end->ends_line
				or $start->has_tag($_Verbatim)
				or $end->has_tag($_Verbatim)  ;
		}
	
		$buffer->remove_all_tags($start, $end);
		$buffer->apply_tag_by_name($tag, $start, $end)
			unless $has_tag or $tag eq 'normal';
		$buffer->set_modified(1);
	
		if ($tag =~ /^head/ and !$has_tag) { # give headers their own line
			$end = $end->ends_line ? undef : $end->get_offset ;
			$buffer->insert($start, "\n") unless $start->starts_line;
			$buffer->insert($buffer->get_iter_at_offset($end+1), "\n")
				unless ! defined $end;
		}

		if ($select eq 'auto') { # unselect
			($start, $end) = $buffer->get_selection_bounds;
			$buffer->select_range($end, $end) if $end;
		}
	}

		$buffer->set_edit_mode_tags($has_tag ? () : ($tag));
	}


=item C<Link(LINK, BOOLEAN)>

This method is called by the "Link" button or by the ^L keybinding.
It makes any selected text a link or toggles the link property.
This link is followed immediatly if the 'follow_new_link' config option is set.

If LINK is undefined the link target is the same as the selected text.

In readonly modus the selected text is regarded as a link and
followed immediatly, but no actual link is made.

BOOLEAN is used to force link (TRUE) or unlink (FALSE). If BOOLEAN is
undefined the function acts as a toggle.

=cut

sub Link {
	my ($self, $link, $bool) = @_;
	my $buffer = $self->{buffer};
	my ($start, $end) = $buffer->get_selection_bounds;
	
	unless ($end and $start != $end) { # no selection
		if (grep {$_->{is_link}} $buffer->get_edit_mode_tags) {
			$bool = 0 unless defined $bool;
			$buffer->set_edit_mode_tags() unless $bool; # reset
			return !$bool;
		}
		elsif ($self->{app}{settings}{use_autoselect}) {
			($start, $end) = $buffer->auto_selection('link');
		}

		unless ($end and $start != $end) {
			return 0 if defined $bool and !$bool;
			my $tag = $buffer->create_link_tag;
			$buffer->set_edit_mode_tags($tag);
			return;
		}
	}

	my $text = $buffer->get_text($start, $end, 0);
	$link = $text unless defined $link;
	return if $link =~ /\n/;
	
	unless (
		$self->{app}{settings}{read_only} or
		$self->{app}{page}{properties}{read_only}
	) {
		return 0 if defined $bool and !$bool;
		my $bit = $link eq $text;
		$buffer->remove_all_tags($start, $end);
		$self->{htext}->apply_link(($bit ? undef : $link), $start, $end);
		$buffer->set_modified(1);
	}

	my ($type, $l) = $self->{app}{page}->parse_link($link);
	$self->{app}->link_clicked($link)
		if $self->{app}{settings}{read_only}
		or $type eq 'page' and $self->{app}{settings}{follow_new_link};
}

=item C<Undo()>

Undo one editing step in the buffer.

=item C<Redo()>

Redo one editing step in the buffer.

=cut

sub Undo {
	my $self = shift;
	$self->{htext}->get_buffer->undo;
}

sub Redo {
	my $self = shift;
	$self->{htext}->get_buffer->redo;
}

sub on_clipboard {
	my ($action, $self) = @_;
	return 0 unless $self->{htext}->get('has-focus');
	my $signal = lc($action->get_name).'_clipboard';
	$self->{htext}->signal_emit($signal);
	return 1;
}

=item C<Delete()>

Delete from cursor.

=cut

sub Delete {
	my $self = shift;
	return unless $self->{htext}->get('has-focus');
	$self->{htext}->signal_emit('delete_from_cursor', 'chars', 1);
}

=item C<EditLink()>

Shows the "Edit link" dialog for the link under the cursor.

=cut

sub EditLink { shift->edit_link_dialog }

=item C<Find(STRING)>

Shows the "Find" bar below the TextView.
STRING is optional query string, which is executed.

=item C<FindNext()>

Repeat previous find in forward direction.

=item C<FindPrev()>

Repeat previous find in backward direction.

=item C<CloseFind()>

Hides the "Find" bar.

=item C<FindReplace(STRING)>

Show the "Find and Replace" dialog.
STRING is the optional query string.

=cut

sub Find {
	my ($self, $query) = @_;
	unless ($self->{entry}->visible) {
		$self->{hbox}->set_no_show_all(0);
		$self->{hbox}->show_all;
		$self->{entry}->grab_focus;
	}
	if (length $query) {
		$self->{entry}->set_text($query);
		$self->_find($query, 0);
	}
}

sub FindNext { $_[0]->_find( $_[0]->{entry}->get_text, 1  ) }

sub FindPrev { $_[0]->_find( $_[0]->{entry}->get_text, -1 ) }

sub _find {
	my $self = shift;
	my $succes = $self->{htext}->search(@_);
	# set buttons active depending on whether we found something or not
	$_->set_sensitive($succes)
		for @{$self}{'find_prev_button', 'find_next_button'};
}

sub CloseFind {
	my $self = shift;
	$self->{hbox}->hide_all;
	$self->{hbox}->set_no_show_all(1);
	$self->{htext}->grab_focus;
}

sub FindReplace {
	$_[0]->{app}->FindReplaceDialog->show($_[1]);
}

=item C<InsertDate(DATE, NO_ASK)>

Inserts a date into the text.
DATE is optional and should be an array ref to the fields
as produced by C<localtime()>.
By default the current date is used.

A dialog with formatting options is presented to the user
unless NO_ASK is true.

=item C<prompt_date_format(DATE)>

Used by C<InsertDate()>.

=cut

sub InsertDate { # insert date that is linked
	my ($self, $date, $no_ask) = @_;
	$date ||= [localtime];
	
	my ($l, $fmt) = $no_ask
		? (1, "%c")
		: $self->prompt_date_format($date) ;
	return unless length $fmt;
	#warn "Got date fmt: $fmt link: $l\n";

	my $buffer = $self->get_buffer;
	my $string = Encode::decode($CODESET, strftime($fmt, @$date));
	my $link = $self->{app}{settings}{cal_namespace}
		. strftime('%Y_%m_%d', @$date) ;
	
	$buffer->insert_blocks_at_cursor(
		($l ? ['link', {to => $link}, $string] : $string), ' ' );
}

sub prompt_date_format {
	my ($self, $date) = @_;
	
	my $dialog = Gtk2::Dialog->new(
		'Insert Date - Zim', $self->{app}{window},
		[qw/modal destroy-with-parent no-separator/],
		'gtk-cancel'  => 'cancel',
	);
	my $button = $self->new_button('gtk-ok', '_Insert');
	$dialog->add_action_widget($button, 'ok');
	
	my $view = Gtk2::SimpleList->new('Date' => 'scalar');
	$view->set_headers_visible(0);
	$view->get_selection->set_mode('browse');
	$view->signal_connect(
		row_activated => sub { $dialog->response('ok') });
	$dialog->vbox->add($view);

	my $check = Gtk2::CheckButton->new('_Link to date');
	my $l = $self->{app}{settings}{recent_date_link};
	$check->set_active($l);
	$dialog->vbox->add($check);

	my ($file) = xdg_data_files('zim', 'dates.list');
	my @dates;
	if (defined $file) {
		for (Zim::File->new($file)->read()) {
			chomp;
			! /^\s*#/ and /\S/ or next;
			push @dates, $_;
		}
	}
	else { @dates = ('%A %d/%m/%Y', '%A %d/%m/%Y %H:%M', '%c') }
	@{$view->{data}} =
		map Encode::decode($CODESET, strftime($_, @$date)),
		@dates ;
	
	my $i = $self->{app}{settings}{recent_date_format};
	$view->select($i) if defined $i;
	
	$dialog->show_all;
	my $r = $dialog->run;
	if ($r eq 'ok') {
		($i) = $view->get_selected_indices;
		$self->{app}{settings}{recent_date_format} = $i;
		$l = $check->get_active;
		$self->{app}{settings}{recent_date_link} = $l;
		$dialog->destroy;
		return ($l, $dates[$i]);
	}
	else {
		$dialog->destroy;
		return undef;
	}
}

=item C<InsertLink(LINK, TEXT, NO_ASK, NL)>

Insert LINK or prompt the user for a link.

NO_ASK is a boolean to suppress the prompt.

=cut

sub InsertLink { 
	my ($self, $link, $text, $no_ask) = @_;
	$text = $link unless length $text;
	if (defined $link and $no_ask) {
		$self->{buffer}->insert_blocks_at_cursor(
			['link', {to => $link}, $text], ' ' );
	}
	else { $self->edit_link_dialog(undef, $link, $text) }
}

=item C<InsertImage(IMAGE, NO_ASK)>

Inserts an image into the buffer.
Without argument prompts the user for a file.

Unless NO_ASK is set it gives a dialog to confirm the action
which allows to set any properties for the image.

=cut

sub InsertImage {
	my ($self, $file, $no_ask) = @_;
	$file = $self->filechooser_dialog unless $file;
	$file = $self->{app}{page}->relative_path($file);
	return unless length $file;

	if ($no_ask) {
		$self->{buffer}->insert_blocks_at_cursor(
			['image', {src => $file}] );
	}
	else {
		$self->edit_image_dialog(undef, $file);
	}
}

sub on_key_press_event_hbox {
	my ($hbox, $event, $self) = @_;
	return 0 unless $event->keyval == $k_escape;
	$self->CloseFind;
	return 1;
}

sub on_activate_entry {
	my ($self, $entry) = @_;
	$self->_find($entry->get_text, 0);
	$self->{htext}->grab_focus;
	$self->CloseFind;
}

=item C<WordCount()>

Pop up a dialog showing word, line and char count.

=cut

sub WordCount {
	my $self = shift;
	my $buffer= $self->{htext}->get_buffer;
	my %stat = (
		lines => $buffer->get_line_count,
		chars => $buffer->get_char_count,
	);

	my ($start, $end) = $buffer->get_bounds;
	my $words = 0;
	while ($start->forward_word_end()) {
		$words++;
	}
	$stat{words} = $words;

	warn "Lines: $stat{lines} Words: $stat{words} Chars: $stat{chars}\n";
	my $dialog = Gtk2::Dialog->new(
		'Word Count - Zim', $self->{app}{window},
		[qw/modal destroy-with-parent no-separator/],
		'gtk-close'  => 'close',
	);
	my $table = Gtk2::Table->new(3, 2);
	$table->set_border_width(5);
	$table->set_row_spacings(5);
	$table->set_col_spacings(12);
	$dialog->vbox->add($table);

	my $i = 0;
	for (qw/lines words chars/) {
		my $label = Gtk2::Label->new();
		$label->set_markup('<b>'.ucfirst($_).':</b>');
		my $align = Gtk2::Alignment->new(0,0.5, 0,0);
		$align->add($label);
		$table->attach_defaults($align, 0,1, $i,$i+1);
		
		$label = Gtk2::Label->new($stat{$_});
		$align = Gtk2::Alignment->new(1,0.5, 0,0);
		$align->add($label);
		$table->attach_defaults($align, 1,2, $i,$i+1);

		$i++;
	}

	$table->show_all;
	$dialog->run;
	$dialog->destroy;
}

=back

=head2 Other Functions

=over 4

=cut

sub on_show_cursor {
	my $self = shift;
	$self->{htext}->set_cursor_visible(
		! $self->{app}{settings}{read_only}
			|| $self->{app}{settings}{ro_cursor} );
}

sub on_edit_mode_changed {
	my ($buffer, $self) = @_;
	my %tags;
	for ($buffer->get_edit_mode_tags) {
		my $name = $_->{is_link} ? 'link' : $_->get_property('name');
		$tags{$name} = 1;
	}

	if ( $tags{link} ) {
		my $data = $self->{htext}->get_link_at_cursor;
		if (defined $data) {
			my $p = $self->{app}{page};
			my ($t, $l) = $p->parse_link($data);
			$l = $p->resolve_name($l) if $t eq 'page';
			$self->{app}->push_status("Go to \"$l\"", 'link');
		}
	}
	else { $self->{app}->pop_status('link') }

	$self->actions_show_active(
		TLink      => $tags{link},
		TBold      => $tags{bold},
		TItalic    => $tags{italic},
		TUnderline => $tags{underline},
		TStrike    => $tags{strike},
	);
}

sub on_toggle_overwrite {
	my $self = pop;
	my $over = $_[0]->get_overwrite;
	my $sbar = $self->{app}{status2};
	my $id = $sbar->get_context_id('over');
	if ($over) { $sbar->push($id, '  OVR') }
	else       { $sbar->pop($id)           }
}

sub on_drag_motion { # debugging purposes only
	my $context = $_[1];
	my @targets = map $_->name, $context->targets;
	warn "Drag motion with targets: @targets\n";
}

sub on_drag_data_received {
	my ($self, $context, $x, $y, $selection, $info, $time) = @_;
	my $type = $selection->target->name;
	my $data = $selection->data;
	#warn "$info ($time): $type: $data\n";

	return unless grep {$_ eq $type}
		qw#text/x-zim-page-list text/uri-list#;
	
	my @items = $self->decode_uri_list($data);
	my @blocks;
	if ($type eq 'text/x-zim-page-list') {
		my $rep = $$self{app}{name};
		@blocks = map {
			if (s/^\Q$rep\E\?//) {
				$_ = $self->{app}{page}->relative_name($_);
			}
			['link', {to => $_}, $_], "\n"
		} @items;
	}
	else { # 'text/uri-list'
		@blocks = map {
			$_ = $self->{app}{page}->relative_path(
					Zim::File->parse_uri($_) ) 
				if m#^file:/#;
			if (m#^\w[\w\+\-\.]+:/#) { # uri
				['link', {to => $_}, $_], "\n";
			}
			else { # path
				my $mtype = mimetype($_);
				($mtype =~ /^image\//)
					? ['image', {src => $_}, $_]
					: ['link',  {to  => $_}, $_]
					, "\n" ;
			}
		} @items;
	}
	
	($x, $y) = $self->{htext}->window_to_buffer_coords('widget', $x, $y);
	my $iter = $self->{htext}->get_iter_at_location($x, $y);
	$self->{buffer}->insert_blocks($iter, @blocks);
}

sub on_populate_popup {
	# add items to context menu
	# object can be a link to a page, a URL (mail / file / http) or an image (which is a file)
	my ($htext, $menu, $self) = @_;
	my $iter = $htext->get_iter_at_pointer;
	return unless $iter;
	
	# source is the filename
	# link is the name as written in the page source
	my ($type, $source);
	my $link = $htext->get_link_at_iter($iter);
	if (defined $link) {
		($type, $source) = $self->{app}{page}->parse_link($link);
	}
	elsif (my $pixbuf = $iter->get_pixbuf) {
		return unless defined $pixbuf->{image_src};
		($type, $source) = ('image', $pixbuf->{image_src});
		$source = $pixbuf->{image_file};
	}
	else { return }
	
	# Separator
	my $seperator = Gtk2::MenuItem->new();
	$seperator->show;
	$menu->prepend($seperator);
	
	# Edit object
	my $edit_item = Gtk2::MenuItem->new(
		($type eq 'image') ? '_Edit Source' : '_Edit Link' );
	$edit_item->show;
	$edit_item->signal_connect_swapped(activate => ($type eq 'image')
		? sub { $self->edit_image_dialog($iter) }
		: sub { $self->edit_link_dialog($iter)  }
		, $self);
	$menu->prepend($edit_item);

	# Copy object
	my $copy_item = Gtk2::MenuItem->new(
		($type eq 'mail') ? 'Copy Email Address' : 'Copy _Link' );
	$copy_item->signal_connect(activate => sub {
		# copy to both clipboard and selection
		for (qw/CLIPBOARD PRIMARY/) {
			my $c = Gtk2::Clipboard->get(
				Gtk2::Gdk::Atom->new($_) );
			$c->set_text($link);
		}
	} );
	$copy_item->show;
	$menu->prepend($copy_item);

	if ($type eq 'image' or $type eq 'file') {
		# Open directory
		my $browse_item = Gtk2::MenuItem->new('Open _Directory');
		$browse_item->signal_connect(activate =>
			sub { $self->{app}->open_directory($source) } );
		$browse_item->show;
		$menu->prepend($browse_item);
		
		return unless $has_mimeapplications
			or $self->{sendto_dir} ;
		
		# Separator
		$seperator = Gtk2::MenuItem->new();
		$seperator->show;
		$menu->prepend($seperator);
	
		# SendTo
		$self->_sendto_menu($menu, $source)
			if $self->{sendto_dir};
		
		# Open with...
		Gtk2::Ex::DesktopEntryMenu->populate_menu($menu, $source)
			if $has_mimeapplications;
	}
	else {
		# Separator
		$seperator = Gtk2::MenuItem->new();
		$seperator->show;
		$menu->prepend($seperator);
	
		# Open link
		my $open_item = Gtk2::MenuItem->new('_Open link');
		$open_item->signal_connect(activate =>
			sub { $self->{app}->link_clicked($link) }  );
		$open_item->show;
		$menu->prepend($open_item);
	}
	
}

sub _sendto_menu {
	my ($self, $menu, $file) = @_;
	$file = Zim::File->abs_path($file);

	my $st_dir = $self->{sendto_dir} || return;
	my @dirs = ($st_dir);
	
	if (my $mt = mimetype($file)) {
		$mt =~ m#(.+?)/(.+)#;
		push @dirs, "$st_dir/.$1" if -d "$st_dir/.$1";
		push @dirs, "$st_dir/.$1_$2" if -d "$st_dir/.$1_$2";
	}
	
	my $st_menu = Gtk2::Menu->new();
	for my $dir (reverse @dirs) {
		for my $script (Zim::File->list_dir($dir)) {
			my $item = Gtk2::MenuItem->new_with_label($script);
			$item->signal_connect(
				activate => sub {Zim::Utils->run("$dir/$script", $file)} );
			$st_menu->append($item);
		}
	}

	my $sep = Gtk2::MenuItem->new();
	$st_menu->append($sep);
	
	my $custom = Gtk2::MenuItem->new('Customise');
	$custom->signal_connect(
		activate => sub {$self->{app}->open_directory($st_dir)} );
	$st_menu->append($custom);

	my $submenu = Gtk2::MenuItem->new('Send To...');
	$submenu->set_submenu($st_menu);
	$menu->prepend($submenu);
	$submenu->show_all;
}

sub on_key_press_event { # some extra keybindings
	# FIXME for more consistent behaviour test for selections
	my ($htext, $event, $self) = @_;
	my $val = $event->keyval;

	if ($self->{app}{settings}{read_only}) {
		# Unix like key bindings for read-only mode
		if ($val == ord '/') {
			$self->Find;
			return 1;
		}
		elsif ($val == ord ' ') {
			my $step = ($event->state >= 'shift-mask') ? -1 : 1 ;
			$self->{htext}->signal_emit('move-cursor', 'pages', $step, 0);
			return 1;
		}
		else { return 0 }
	}
	
	if ($val == $k_return or $val == $k_kp_enter) { # Enter
		my $buffer = $htext->get_buffer;
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		#return 1 if defined $htext->click_if_link_at_iter($iter); # ?
		$self->parse_word($iter); # end-of-line is also end-of-word
		$iter = $buffer->get_iter_at_mark($buffer->get_insert());
		$self->parse_line($iter) or return 0;
		$htext->scroll_mark_onscreen( $buffer->get_insert );
		return 1;
	}
	elsif (
		$self->{app}{settings}{backsp_unindent} and $val == $k_backspace
		or $val == $k_l_tab
		or $val == $k_tab and $event->state >= 'shift-mask'
	) { # BackSpace or Shift-Tab
		my $buffer = $htext->get_buffer;
		my ($start, $end) = $buffer->get_selection_bounds;
		if ($end and $end != $start) {
			my $cont = $self->selection_backspace($start, $end);
			return $val == $k_tab ? 1 : $cont;
		}
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		if ($self->parse_backspace($iter)) {
			$htext->scroll_mark_onscreen( $buffer->get_insert );
			return 1;
		}
	}
	elsif ($val == $k_tab or $val == ord(' ')) { # WhiteSpace
		my $buffer = $htext->get_buffer;
		if ($val == $k_tab) {
			my ($start, $end) = $buffer->get_selection_bounds;
			if ($end and $end != $start) {
				$self->selection_tab($start, $end);
				return 1;
			}
		}
		my $iter = $buffer->get_iter_at_mark($buffer->get_insert());
		my $string = ($val == $k_tab) ? "\t" : ' ';
		if ($self->parse_word($iter, $string)) {
			$htext->scroll_mark_onscreen( $buffer->get_insert );
			return 1;
		}
	}
	elsif ($val == ord('*') or $val == $k_multiply) { # Bullet
		my $buffer = $htext->get_buffer;
		my ($start, $end) = $buffer->get_selection_bounds;
		return 0 if !$end or $end == $start;
		$self->toggle_bullets($start, $end);
		return 1;
	}
	elsif ($val == $k_home and not $event->state >= 'control-mask') { # Home toggle
		my $buffer = $htext->get_buffer;
		my $insert = $buffer->get_iter_at_mark($buffer->get_insert());
		my $start  = $insert->copy;
		$htext->backward_display_line_start($start)
			unless $htext->starts_display_line($start);
		my $begin  = $start->copy;
		my $indent = '';
		while ($indent =~ /^\s*([^\s\w]\s*)?$/) {
			last if $begin->ends_line or ! $begin->forward_char;
			$indent = $start->get_text($begin);
		}
		$indent =~ /^(\s*([^\s\w]\s+)?)/;
		my $back = length($indent) - length($1);
		$begin->backward_chars($back) if $back > 0;
		$insert = ($begin->ends_line || $insert->equal($begin)) ? $start : $begin;
		if ($event->state >= 'shift-mask') {
			$buffer->move_mark_by_name('insert', $insert);
			# leaving the "selection_bound" mark behind
		}
		else { $buffer->place_cursor($insert) }
		return 1;
	}
		
	#else { printf "key %x pressed\n", $val } # perldoc -m Gtk2::Gdk::Keysyms

	return 0;
}

=item C<set_font(STRING)>

Set the default font for the pageview.
Calling with C<undef> as argument will reset to the default.

=cut

sub set_font {
	my ($self, $string) = @_;
	my $font = defined($string)
		? Gtk2::Pango::FontDescription->from_string($string)
		: undef ;
	$self->{htext}->modify_font($font);
}

=item C<get_state()>

Returns a HASH ref with properties that need to be saved in the history.

=cut

sub get_state {
	my $self = shift;
	my $buffer = $self->{buffer} || return;
	my $cursor = $buffer->get_iter_at_mark($buffer->get_insert)->get_offset;
	my $vscroll = $self->{scrolled_window}->get_vadjustment->get_value;
	my $hscroll = $self->{scrolled_window}->get_hadjustment->get_value;
	my %rec = (cursor  => $cursor,
	           vscroll => $vscroll,
		   hscroll => $hscroll  );
	#warn "get_state cursor @ $cursor ($vscroll, $hscroll)\n";
	return \%rec;
}

=item C<set_state(\%PROPETIES)>

Set a number of properties that were saved in the history.

=cut

sub set_state {
	my $self = shift;
	my %rec = %{shift()};
	#warn "set state @ $rec{cursor} (@rec{'vscroll', 'hscroll'})\n";
	if (defined $rec{cursor}) {
		$self->{buffer}->place_cursor(
			$self->{buffer}->get_iter_at_offset($rec{cursor}) );
	
		$self->{htext}->scroll_mark_onscreen( $self->{buffer}->get_insert );
	}
	if (defined $rec{vscroll}) {
		my $vadj = $self->{scrolled_window}->get_vadjustment;
		$vadj->set_value($rec{vscroll});
		$vadj->value_changed;
	}
	if (defined $rec{hscroll}) {
		my $hadj = $self->{scrolled_window}->get_hadjustment;
		$hadj->set_value($rec{hscroll});
		$hadj->value_changed;
	}
}

=item C<load_page(PAGE)>

Load a new page in the page view widgets.

=cut

sub load_page {
	my ($self, $page) = @_;
	my $mode = $page->properties->{view};
	$self->on_exit_mode
		if $self->{mode} and $mode ne $self->{mode};
	if ($mode and $mode ne $self->{mode}) { # to special mode
		my $class = 'Zim::GUI::PageView::'.ucfirst(quotemeta($mode));
		eval "use $class";
		unless ($@) {
			bless $self, $class;
			$self->on_enter_mode;
		}
		else { warn $@ }
	}
	elsif ($self->{mode} and ! $mode) { # to default mode
		bless $self, 'Zim::GUI::PageView';
	}
	$self->{mode} = $mode;
	$self->load_page_contents($page);
}

sub on_enter_mode {} # stub to be overloaded

sub on_exit_mode {} # stub to be overloaded

=item C<load_page_contents(PAGE, TREE)>

To be overloaded by child classes, use C<load_page()> from the GUI.

Load the contents of PAGE into the buffer.
This method should die when an error is encountered.

TREE is an optional argument, child classes do not need to support it.

=cut

sub load_page_contents {
	my ($self, $page, $tree) = @_;
	my $use_spell = defined $self->{app}{objects}{Spell}; # FIXME ugly internals
	$tree ||= $page->get_parse_tree(); 
	
	# clear the old buffer
	$self->{buffer}->clear if $self->{buffer};
	$self->{app}->Spell->detach($self->{htext}) if $use_spell;
	$self->{_prev_buffer} = $self->{buffer}; # FIXME hack to prevent segfaults
	
	# create a new HyperTextBuffer
	my $buffer = Gtk2::Ex::HyperTextBuffer->new();
	$buffer->create_default_tags;
#	$buffer->create_tag('info', editable => 0); #, foreground => 'red');
	$self->{buffer} = $buffer;
	$self->{htext}->set_buffer($buffer);
	$buffer->set_parse_tree( $tree );
	unless ($self->{app}{settings}{read_only}) {
		# connect signals _after_ load_parsetree()
		$buffer->signal_connect(modified_changed =>
			sub {$self->{app}->update_status} );
		$buffer->signal_connect(edit_mode_changed => \&on_edit_mode_changed, $self);
	}
	$buffer->{_zim_name} = $self->{app}{name}; # FIXME FIXME FIXME
	$self->{app}->Spell->attach($self->{htext}) if $use_spell;
	
	if ($page->exists) {
		$buffer->place_cursor(
	 	      	$buffer->get_iter_at_offset(0) );
	}
	else { # new page, place cursor below headers
		my (undef, $iter) = $buffer->get_bounds;
		$buffer->place_cursor( $iter );
	}
	on_edit_mode_changed($buffer, $self)
		unless $self->{app}{settings}{read_only}; # reset format buttons
	
	$self->{htext}->scroll_mark_onscreen( $buffer->get_insert );

	$buffer->set_modified(0);
	$self->_match_all_words($buffer, $page)
		if $self->{app}{settings}{use_autolink} && $page->exists;
}

=item C<modified()>

=item C<modified(BOOLEAN)>

Get or set the modified bit. This bit is set when the buffer
is modified by the user.
It should be reset after succesfully saving the buffer content.

=cut

sub modified {
	return 0 unless defined $_[0]->{buffer};
	$_[0]->{buffer}->set_modified($_[1]) if defined $_[1];
	$_[0]->{buffer}->get_modified;
}

=item C<save_page(PAGE)>

Save current buffer contents to PAGE.

=cut

sub save_page {
	my ($self, $page) = @_;
	my $existed = $page->exists;
	my $ack = $self->save_page_contents($page);
	if ($ack) {
		my $act = $existed ? 'page_saved' : 'page_created';
		$self->{app}->signal_emit($act => $page->name);
	}
	else { # page was empty => delete instead of save
		$page->delete;
		$self->{app}->signal_emit('page_deleted', $page->name);
	}
	$self->modified(0);
}

=item C<save_page_contents(PAGE)>

To be overloaded by child classes, use C<save_page()> from the GUI.

This method saves the buffer contents to PAGE.
Should return 1 on success or 0 when the buffer was empty.
Returning 0 results in the page being deleted.

This method should die when an error is encountered.

=cut

sub save_page_contents {
	my ($self, $page) = @_;
	my $tree = $self->{buffer}->get_parse_tree;
	return 0 unless @$tree > 2 ; # check content in buffer
	$page->set_parse_tree($tree);
	return 1;
}

=item C<change_widget(WIDGET)>

Replaces the curren page view widget in the interface with WIDGET.
Returns the old widget.

=cut

sub change_widget {
	my ($self, $widget) = @_;
	my $old = $self->{top_widget};
	#warn "switching $old for $widget\n";
	$self->{app}{r_vbox}->remove($old);
	$self->{app}{r_vbox}->pack_end($widget, 1,1,0);
	$self->{top_widget} = $widget;
	$self->{app}{vbox}->set_focus_chain($self->{app}{l_vbox}, $widget);
	return $old;
}

=item C<parse_backspace(ITER)>

This method is called when the user types a backspace.
It tries to update the formatting of the current line.
 
When TRUE is returned the widget does not receive the backspace.

=cut

sub parse_backspace {
	my ($self, $iter) = @_;
	my $buffer = $self->{buffer};
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	if ($line =~ s/\t([\*\x{2022}]\s)$/$1/) {
		_user_action_ {
			$buffer->delete($lf, $iter);
			$buffer->insert($lf, $line);
		} $buffer;
		return 1;
	}
	elsif ($line =~ /\t$/) {
		my $back = $iter->copy;
		$back->backward_char;
		_user_action_ { $buffer->delete($back, $iter) } $buffer;
		return 1;
	}
	return 0;
}

=item C<parse_line(ITER)>

This method is called when the user is about to insert a linebreak.
It checks the line left of the cursor of any markup that needs 
updating. It also takes care of autoindenting.

When TRUE is returned the widget does not receive the linebreak.

=cut

sub parse_line {
	my ($self, $iter) = @_;
	my $buffer = $self->{buffer};
	my $Verbatim = $buffer->get_tag_table->lookup('Verbatim');
	$buffer->set_edit_mode_tags(
		grep {$_ eq $Verbatim}
		$buffer->get_edit_mode_tags() ); # reset all tags except Verbatim
	my $lf = $buffer->get_iter_at_line( $iter->get_line );
	my $line = $buffer->get_text($lf, $iter, 0);
	#print ">>$line<<\n";
	if ($line =~ s/^(=+)\s*(\w)/$2/) { # heading
		my $offset;
		($lf, $offset) = ($lf->get_offset, $iter->get_offset);
		_user_action_ { $buffer->insert($iter, "\n") } $buffer;
		($lf, $iter) = map $buffer->get_iter_at_offset($_), $lf, $offset;
		$iter->forward_char;
		my $h = length($1); # no (7 - x) monkey bussiness here
		$h = 5 if $h > 5;
		$line =~ s/\s+=+\s*$//;
		$offset = $lf->get_offset + length $line;
		_user_action_ {
			$buffer->delete($lf, $iter);
			$buffer->insert_with_tags_by_name($lf, $line, "head$h");
			$iter = $buffer->get_iter_at_offset($offset);
			$buffer->insert($iter, "\n");
		} $buffer;
		return 1;
	}
	elsif ($line =~ /^(\s*(:?\W+|\d+\W?|\w\W)\s+|\s+)$/) { # empty bullet or list item
		# TODO check previous line for same pattern !
		_user_action_ { $buffer->delete($lf, $iter) } $buffer;
	}
	elsif ($line =~ /^(\s*(:?\W+|\d+\W?|\w\W)\s+|\s+)/) { # auto indenting + lists
		my ($indent, $number) = ($1, $2);
		if (length $number) { # numbered list
			$number = ($number =~ /\d/) ? $number+1 : chr(ord($number)+1);
			$indent =~ s/^(\s*)(\d+|\w)/$1$number/;
		}
		my $offset = $iter->get_offset;
		_user_action_ { $buffer->insert($iter, "\n") } $buffer;
		$iter = $buffer->get_iter_at_offset($offset);
		$iter->forward_char;
		_user_action_ { $buffer->insert($iter, "$indent") } $buffer;
		$self->{htext}->scroll_mark_onscreen( $buffer->get_insert() );
		return 1;
	}
	return 0;
}

=item C<parse_word(ITER, CHAR)>

This method is called after the user ended typing a word.
It checks the word left of the cursor for any markup that
needs updating.

CHAR can be the key that caused a word to end, returning TRUE
makes it never reaching the widget.

=cut

sub parse_word {
	# remember that $char can be empty
	# first insert the char, then replace it, keep undo stack in proper order
	my ($self, $iter, $char) = @_;
	return 0 if $self->_is_verbatim($iter);
	my $buffer = $self->{buffer};
	my $lf = $iter->copy;
	$self->{htext}->backward_display_line_start($lf)
		unless $self->{htext}->starts_display_line($lf);
	my $line = $buffer->get_text($lf, $iter, 0);
	#warn ">>$line<< >>$char<<\n";
	if ($line =~ /^(\s*)[\*\x{2022}](\s*)$/) { # bullet
		return unless $lf->starts_line; # starts_display_line != starts_line
		my ($pre, $post) = ($1, $2.$char);
		my $offset;
		if (defined $char) {
			($lf, $offset) = ($lf->get_offset, $iter->get_offset);
			_user_action_ { $buffer->insert($iter, $char) } $buffer;
			($lf, $iter) = map $buffer->get_iter_at_offset($_), $lf, $offset;
			$iter->forward_char;
			$pre .= $1 if $post =~ s/(\t+)$//; # switch tabs
			$post = ' ' unless length $post;
		}
		$line = $pre."\x{2022}".$post;
		_user_action_ {
			$buffer->delete($lf, $iter);
			$buffer->insert($lf, $line);
		} $buffer;
		return 1;
	}
	elsif (
		$line =~ qr{(?<!\S)(\w[\w\+\-\.]+://\S+)$} # url
		or $line =~ qr{ (?<!\S)(
		        [\w\.\-\(\)]*(?: :[\w\.\-\(\)]{2,} )+:?
		      | \.\w[\w\.\-\(\)]+(?: :[\w\.\-\(\)]{2,} )*:?
		            )$  }x # page (ns:page .subpage)
		or $line =~ qr{(?<!\S)(
		      \w[\w\+\-\.]+\?\S+
		            )$}x # interwiki (name?page)
		or ( $self->{app}{settings}{use_linkfiles}
		  and $line =~ qr{ (?<!\S)( (?:
	  	      ~/[^/\s] | ~[^/\s]*/ | \.\.?/ | /[^/\s]
		               ) \S* )$  }x ) # file (~/ ~name/ ../ ./ /)
		or ( $self->{app}{settings}{use_camelcase}
		  and $line =~ qr{(?<!\S)(
	  	      [[:upper:]]+[[:lower:]]+[[:upper:]]+\w*
		               )$}x  ) # CamelCase
	) { # any kind of link
		my $word = $1;
		return 0 if $word !~ /[[:alpha:]]{2}/; # at least two letters in there
		#return 0 unless $word =~ /[^:\dhu]/i; # do not link "10:20h"
		my ($start, $end) = ($iter->copy, $iter->copy);
		$start->backward_chars(length $word);
		return 0 if grep {$_->get_property('name') !~ /spell/}
			$start->get_tags, $end->get_tags;
		if (defined $char) {
			($start, $end) = ($start->get_offset, $end->get_offset);
			_user_action_ { $buffer->insert($iter, $char) } $buffer;
			($start, $end) = map $buffer->get_iter_at_offset($_), $start, $end;
		}
		_user_action_ {
			$self->{htext}->apply_link(undef, $start, $end);
		} $buffer;
		return 1;
		
	}
	elsif ( $self->{app}{settings}{use_utf8_ent} &&
		$line =~ /(?<!\S)\\(\w+)$/
	) { # utf8 chars
		my $word = $1;
		my $chr = _entity($word);
		return 0 unless defined $chr;
		
		if (defined $char) {
			my $offset = $iter->get_offset;
			_user_action_ { $buffer->insert($iter, $char) } $buffer;
			$iter = $buffer->get_iter_at_offset($offset)
		}
		my $begin = $iter->copy;
		$begin->backward_chars(1 + length $word);
		_user_action_ {
			$buffer->delete($begin, $iter);
			$buffer->insert($begin, $chr);
		} $buffer;
		return 1;
	}
#	elsif ($line =~ /^(\t|  )/) { # pre
#		# FIXME \s* => \t
#		$iter->forward_char unless $iter->is_end; # FIXME somthing at end
#		$buffer->apply_tag_by_name('pre', $lf, $iter);
#	}
	elsif ($self->{app}{settings}{use_autolink}) {
		$self->_match_words($buffer, $iter);
	}
	
	return 0;
}

my %_entities;

sub _entity {
	my $key = shift;
	return chr($key) if $key =~ /^\d+$/;
	my $ord;
	if (exists $_entities{$key}) { return $_entities{$key} }
	else {
		for (xdg_data_files(qw/zim entities.list/)) {
			$ord = Zim::File->new($_)->read_hash($key, 1);
			last if defined $ord;
		}
	}
	return unless length $ord;
	$ord =~ /(\d+)/; # ignore comments
	return chr($1);
}

sub _match_words {
	my ($self, $buffer, $iter, $page) = @_;
	return if $iter->starts_line;
	$page ||= $self->{app}{page};
	my $start = $iter->copy;
	$start->backward_chars( $iter->get_line_offset );
	my $line = $start->get_text($iter);
	
	while ($line =~ /\w/) {
		my ($word) = ($line =~ /(\w+)/);
		#warn "Checking: >>$word<<\n";
		while ($_ = $page->match_word($word)) {
			warn "Matching: $_ for >>$word<<\n";
			# match_word returns 1 for single match
			#            and 2 for multiple or partial matches
			if ($_ == 1) {
				my $start = $iter->copy;
				$start->backward_chars(length $line);
				my $end = $start->copy;
				$end->forward_chars(length $word);
				last if $start->get_tags or $end->get_tags;
				$self->{htext}->apply_link(undef, $start, $end);
				last;
			}
			else {
				($word) = ($line =~ /(\Q$word\E\W+\w+)/);
				last unless $word;
			}
		}
		$line =~ s/^\W*\w+\W*//;
		#warn "line: >>$line<<\n";
	}
}

sub _match_all_words { # call _match_words on all lines
	my ($self, $buffer, $page) = @_;
	my ($iter, undef) = $buffer->get_bounds;
	while ($iter->forward_to_line_end) {
		$self->_match_words($buffer, $iter, $page);
	}
}

sub _is_verbatim {
	my ($self, $iter) = @_;
	for ($iter->get_tags) {
		return 1 if lc($_->get_property('name')) eq 'verbatim';
	}
	return 0;
}

=item C<insert_file(FILE)>

Inserts a link to a file into the buffer.

=cut

sub insert_file {
	my ($self, $file) = @_;
	return unless length $file;
	die 'TODO'
}

=item C<toggle_bullets()>

If selected text is a bullet list this removes the bullets, else it adds
bullets.

=cut

sub toggle_bullets {
	my ($self, $start, $end) = @_;
	my $buffer = $self->{buffer};
	($start, $end) = $buffer->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;
	
	my $text = $self->{buffer}->get_text($start, $end, 1);
	if ($text =~ /^\s*[\*\x{2022}]\s+/m) { # remove bullets
		$text =~ s/^(\s*)[\*\x{2022}]\s+/$1/mg
	}
	else { # set bullets
		$text =~ s/^(\s*)(\S)/$1\x{2022} $2/mg;
	}

	_user_action_ { $buffer->replace_selection($text) } $buffer;
}

=item C<selection_tab()>

Puts a tab before every line of a selection.

=cut

sub selection_tab {
	my ($self, $start, $end) = @_;
	($start, $end) = $self->{buffer}->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;
	
	my $buffer = $self->{buffer};
	my $text = $buffer->get_text($start, $end, 1);
	$text =~ s/^/\t/mg;

	my $verbatim = $self->_is_verbatim($start);
	_user_action_ {
	($start, $end) = $buffer->replace_selection($text);
	$self->{buffer}->apply_tag_by_name('verbatim', $start, $end)
		if $verbatim;
	} $buffer;
}

=item C<selection_backspace()>

Removes a tab for every line of a selection.

=cut

sub selection_backspace {
	my ($self, $start, $end) = @_;
	my $buffer = $self->{buffer};
	($start, $end) = $buffer->get_selection_bounds unless defined $start;
	return if !$end or $start == $end;

	my $text = $self->{buffer}->get_text($start, $end, 1);
	my $verbatim = $self->_is_verbatim($start);
	if ($text =~ s/^\t//mg) {
		_user_action_ {
		($start, $end) = $buffer->replace_selection($text);
		$self->{buffer}->apply_tag_by_name('Verbatim', $start, $end)
			if $verbatim;
		} $self->{buffer};
		return 1;
	}

	return 0;
}

=item C<edit_link_dialog(ITER)>

This dialog allows the user to create a link for which
the link target and the link text differ.

If ITER is undefined the cursor is used.

=cut

sub edit_link_dialog { # TODO this routine is also use by InsertLink, which is conflicting behavior
	my ($self, $iter, $link, $text) = @_;
	my $buffer = $self->{buffer};

	# handle selections
	my ($start, $end) = $buffer->auto_selection('link', $iter);

	$link = '' unless length $link;
	$text = $link unless length $text;
	if (defined $start) {
		$link = $self->{htext}->get_link_at_iter($start);
		$text = $self->{buffer}->get_text($start, $end, 0);
		$text = undef if $text =~ /\n/;
	}

	# Run the dialog
	my $val = $self->run_prompt(
		(defined($start) ? 'Edit Link' : 'Insert Link'),
		['txt', 'link'], {
			txt  => ['Text', 'string', $text],
			link => ['Links to', 'page', $link],
		}, 'gtk-connect', '_Link' )
		or return;
	($text, $link) = @$val;
	return unless $text =~ /\S/ or $link =~ /\S/;
	
	# both entries default to the other
	$link = $text unless $link =~ /\S/;
	$text = $link unless $text =~ /\S/;

	# use delete + insert instead of apply because the text can be different
	if (defined $start) {
		$buffer->delete($start, $end);
	}
	else {
		$start = $buffer->get_iter_at_mark( $buffer->get_insert());
	}
	my $bit = $link eq $text;
	my $_start = $start->get_offset;
	$buffer->insert($start, $text);
	$start = $buffer->get_iter_at_offset($_start);
	$end = $start->copy;
	$end->forward_chars(length $text);
	$buffer->remove_all_tags($start, $end); # because of this we can't use htext->insert_link()
	$self->{htext}->apply_link(($bit ? undef : $link), $start, $end);
	# FIXME redundant code from "apply_link" here
}

=item C<edit_image_dialog(ITER, SRC)>

Popup a dialog that allows changing the source and properties
of an image. Both arguments are optional.

=cut

sub edit_image_dialog {
	my ($self, $iter, $src) = @_;

	my $pixbuf = $iter ? $iter->get_pixbuf : undef ;
	$src = $pixbuf->{image_src} if $pixbuf and ! length $src;
	return if $pixbuf and ! length $src;
	
	my ($dialog, $entries) = $self->new_prompt(
		'Edit Image Source',
		['src'], {src => ['Source', 'file', $src]},
		'gtk-connect', '_Link' );
	
	my ($w, $h, $ratio) = (0, 0, 1);
	if ($pixbuf) {
		($w, $h) = ($pixbuf->get_width, $pixbuf->get_height);
		$ratio = $w / $h;
	}

	my $frame = Gtk2::Expander->new_with_mnemonic('_Properties');
	$dialog->vbox->add($frame);
	my $vbox = Gtk2::VBox->new();
	$vbox->set_border_width(12);
	$frame->add($vbox);

	my $table = Gtk2::Table->new(2, 2, 0);
	$table->set_row_spacings(5);
	$table->set_col_spacings(12);
	$vbox->add($table);

	my $i = 0;
	for (qw/Width Height/) {
		my $label = Gtk2::Label->new($_.':');
		my $align = Gtk2::Alignment->new(0,0.5, 0,0);
		$align->add($label);
		$table->attach_defaults($align, 0,1, $i,$i+1);
		$i++;
	}

	my $input_w = Gtk2::SpinButton->new_with_range(1, 4*$w || 1, 1);
	$input_w->set_value($w);
	$table->attach_defaults($input_w, 1,2, 0,1);
	my $input_h = Gtk2::SpinButton->new_with_range(1, 4*$h || 1, 1);
	$input_h->set_value($h);
	$table->attach_defaults($input_h, 1,2, 1,2);

	my $reset_button = Gtk2::Button->new('_Reset');
	$vbox->pack_start($reset_button, 0,0,0);

	my ($scale, $block) = ('', 0);
	if ($pixbuf and $pixbuf->{resized}) {
		$scale = $pixbuf->{resized}[0] ? 'w' : 'h';
	}
	$input_w->signal_connect(value_changed => sub {
		return if $block;
		$scale = 'w';
		$w = $input_w->get_value;
		$h = int $w / $ratio;
		$block = 1;
		$input_h->set_value($h);
		$block = 0;
	} );
	$input_h->signal_connect(value_changed => sub {
		return if $block;
		$scale = 'h';
		$h = $input_h->get_value;
		$w = int $ratio * $h;
		$block = 1;
		$input_w->set_value($w);
		$block = 0;
	} );
	my $init = sub {
		my $file = $$entries[0]->get_text;
		$file = File::Spec->rel2abs($file, $self->{buffer}{base})
			if $self->{buffer}{base};
		eval { # dies on my win32 installation
			(undef, $w, $h) = (-f $file and -r _)
				? (Gtk2::Gdk::Pixbuf->get_file_info($file))
				: (0,0) ;
		};
		if ($w && $h) {
			$_->set_sensitive(1)
				for $reset_button, $input_w, $input_h;
			$ratio = $w / $h;
			$block = 1;
			$input_w->set_range(1, 4*$w);
			$input_w->set_value($w);
			$input_h->set_range(1, 4*$h);
			$input_h->set_value($h);
			$block = 0;
			$scale = '';
		}
		else {
			$_->set_sensitive(0)
				for $reset_button, $input_w, $input_h;
		}
	};
	$reset_button->signal_connect(clicked => $init);
	$$entries[0]->signal_connect(changed => $init);
	$init->() unless $pixbuf;

	$dialog->show_all;
	my $r = $dialog->run;
	$dialog->destroy;
	return unless $r eq 'ok';

	my $new_src = $$entries[0]->get_text;
	return unless $new_src =~ /\S/;

	my $buffer = $self->{buffer};
	if ($iter and $pixbuf) { # delete image
		my $end = $iter->copy;
		$end->forward_char;
		$buffer->delete($iter, $end);
		$buffer->place_cursor($iter);
	}
	my @arg = $scale
		? ($new_src, ($scale eq 'w') ? ($w, 0) : (0, $h))
		: ($new_src)  ;
	my %m;
	@m{'src', 'width', 'height'} = @arg;
	$self->{buffer}->insert_blocks_at_cursor(['image', \%m]);
}

1;

__END__

=back

=head1 AUTHOR

Jaap Karssenberg (Pardus) E<lt>pardus@cpan.orgE<gt>

Copyright (c) 2005 Jaap G Karssenberg. All rights reserved.
This program is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.

=head1 SEE ALSO

=cut

